#!/bin/sh

# Copyright (C) 2018 Alister Sanders

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

SP_MACRO_PATH="$SP_MACRO_PATH:/usr/share/spotfile"
SP_PKG_PATH="$SP_PKG_PATH:$PWD"

RED="\\x1b[38;5;88m"
BLUE="\\x1b[38;5;69m"
GREEN="\\x1b[38;5;107m"
RESET="\\x1b[0m"

# Utility Functions
# ------------------------------------------------------------------------------

highlight() {
        echo -e $(echo "$@" |
		      sed -e "s,//\([^\(//\)]*\)//,$BLUE\1$RESET,g" \
			  -e "s,##\([^\(##\)]*\)##,$RED\1$RESET,g")
}

log() {
    case "$1" in
     	error) shift;          highlight " $RED!$RESET $@" >&2 ;;
	info)  shift;          highlight " $GREEN*$RESET $@" ;;
	verb)  shift;          [ "$verbose" = 1 ] && highlight "$@" || : ;;
	*) ;;
    esac
}

die() {
    log error "$@"
    exit 1
}

find_macro_file() {
    fullpath=""

    # Check each directory specified in SP_MACRO_PATH for the file
    for path in `echo "$SP_MACRO_PATH" | sed -e 's/:/\n/g'`; do
	if [ ! -z "$path" -a -f "$path/$1" ]; then
	    fullpath="$path/$1"
	    break
	fi
    done

    if [ ! -z "$fullpath" ]; then
	echo "$fullpath"
	return 0
    else
	return 1
    fi
}

require() {
    which "$1" &>/dev/null ||
	die "Could not find //$1// in //\$PATH// (is it installed?)"
}

strip_json_str() {
    # Remove leading/trailing quotation marks
    echo -n "$1" | sed -e 's/^"\(.*\)"$/\1/'
}

run_m4() {
    working_dir="$1"
    target_host="$2"
    
    # TODO: Include spotfile version info
    (cd "$working_dir"; while read -r line; do
			    echo "$line"
			done | M4PATH="$SP_MACRO_PATH" m4 -P \
				    -D SP_BUILDHOST="$(hostname)" \
				    -D SP_TARGETHOST="$target_host")
}

help() {
    echo "$(basename $0) [ARGS] PACKAGE1 PACKAGE2 ..."
    echo "  -t, --target HOST      Add a target host"
    echo "  -i, --install          Install the specified packages"
    echo "  -S, --no-symlink       Don't create symlinks during package"
    echo "                         installation; copy files directly"
    echo "  -r, --relative         Use relative symlinks for installation"
    echo "  -V, --verbose          Print verbose messages"
    echo "  -h, --help             Print help message"
}

# Package operations
# ------------------------------------------------------------------------------

find_package() {
    for path in `echo "$SP_PKG_PATH" | sed -e 's/:/\n/g'`; do
	if [ ! -z "$path" -a -d "$path/$1" ]; then
	    fullpath="$path/$1"
	    break
	fi
    done

    if [ ! -z "$fullpath" ]; then
	echo "$fullpath"
	return 0
    else
	return 1
    fi
}

build_package() {
    pkg="$1"
    target_host="$2"
    sp_macros="$3"
    
    log verb "Check for existence of //$pkg/// in //\$SP_PKG_PATH//"

    pkg_path=$(find_package "$pkg")
    [ $? -ne 0 ] &&
	die "Could not find //$pkg// in //\$SP_PKG_PATH//" ||
	    log verb "Found //$pkg//: $pkg_path"

    manifest_file="$pkg_path/pkg.json"
    [ ! -f "$manifest_file" ] &&
	die "//$pkg// doesn't have a package manifest ($manifest_file)!" ||
	    log verb "Found manifest for //$pkg//: $manifest_file"

    manifest=$(cat "$manifest_file" | jq -c)
    build_targets=$(echo "$manifest" | jq ".targets")
    n_targets=$(echo "$build_targets" | jq length)

    log verb "$n_targets target(s) total to build"

    if [ "$n_targets" = 0 ]; then
	log verb "//$pkg// has no targets --- nothing to do"
	return 0
    fi

    for i in `seq 0 $((n_targets - 1))`; do
	# Determine the input and output file for this target
	in_file=$(strip_json_str "$(echo "$build_targets" | jq ".[$i].in")")
	out_file=$(strip_json_str "$(echo "$build_targets" | jq ".[$i].out")")

	log verb "Target wants $in_file -> $out_file"
	input_file_path="$pkg_path/$in_file"
	output_file_path="$pkg_path/$target_host/$out_file"
	output_dir=$(dirname "$output_file_path")

	log verb "Check whether $output_dir exists..."
	if [ ! -d "$output_dir" ]; then
	    mkdir -p "$output_dir" &&
		log verb "Created directory $output_dir" ||
		    die "Failed to create $output_dir for //$pkg//"
	fi

	# Finally, build the target
	cat "$sp_macros" "$input_file_path" |
	    run_m4 "$pkg_path" "$target_host" > "$output_file_path"
    done
}

install_package() {
    pkg="$1"
    target_host="$2"
    no_symlink="$3"
    relative_symlink="$4"
    
    log verb "Check for existence of //$pkg/// in //\$SP_PKG_PATH//"

    pkg_path=$(find_package "$pkg")
    [ $? -ne 0 ] &&
	die "Could not find //$pkg// in //\$SP_PKG_PATH//" ||
	    log verb "Found //$pkg//: $pkg_path"

    manifest_file="$pkg_path/pkg.json"
    [ ! -f "$manifest_file" ] &&
	die "//$pkg// doesn't have a package manifest ($manifest_file)!" ||
	    log verb "Found manifest for //$pkg//: $manifest_file"

    manifest=$(cat "$manifest_file" | jq -c)
    install_targets=$(echo "$manifest" | jq ".install")
    n_targets=$(echo "$install_targets" | jq length)

    if [ "$n_targets" = 0 ]; then
	log info "//$pkg// has no installation targets... eh?"
	return 0
    fi

    log verb "Check for host directory in $pkg_path"
    [ ! -d "$pkg_path/$target_host" ] && die "//$pkg// does not include a configuration for host //$target_host//"
    
    for i in `seq 0 $((n_targets - 1))`; do
	source_file=$(strip_json_str $(echo "$install_targets" | jq ".[$i].source"))
	dest_file=$(strip_json_str $(echo "$install_targets" | jq ".[$i].destination") | sed -e 's,/$,,')

	source_file_path="$pkg_path/$target_host/$source_file"
	dest_file_path="${dest_file/#\~/$HOME}"
	dest_parent="$(dirname $dest_file_path)"

	# Make sure the parent of the destination path actually exists
	if [ ! -d "$dest_parent" ]; then
	    mkdir -p "$dest_parent" &&
		log verb "Created $dest_parent" ||
		    die "Could not create $dest_parent"
	fi

	log verb "Installation target wants $source_file_path -> $dest_file_path"

	# Any trailing slashes were removed from the path, so it's
	# safe to run rm -rf without fear of deleting contents of a
	# symlinked directory
	if [ -e "$dest_file_path" -o -h "$dest_file_path" ]; then
	    log info "Removing old $dest_file_path"
	    rm -rf "$dest_file_path"
	fi
	
	if [ "$no_symlink" = 1 ]; then
	    cp -r "$source_file_path" "$dest_file_path" &&
		log info "Copied $source_file_path -> $dest_file_path" ||
		    die "Failed to copy $source_file_path -> $dest_file_path"
	elif [ "$relative_symlink" = 1 ]; then
	    ln -rs "$source_file_path" "$dest_file_path" &&
		log info "Linked $dest_file_path -> $source_file_path" ||
		    die "Failed to link $dest_file_path -> $source_file_path"
	else
	    ln -s "$source_file_path" "$dest_file_path" &&
		log info "Linked $dest_file_path -> $source_file_path" ||
		    die "Failed to link $dest_file_path -> $source_file_path"
	    
	fi
    done
}

# Main
# -------------------------------------------------------------------------------

require getopt
require m4
require jq

# Make sure getopt long arguments are supported
getopt -T; [ $? = 4 ] || die "Long getopt arguments are not supported"

# Process command line options
GETOPT=`getopt -o "t:iSrVh" --long "target:,install,no-symlink,relative,verbose,help" -- $*`
eval set -- "$GETOPT"
while :; do
    case "$1" in
	-t|--target)
	    # Append this host to the list of target hosts
	    [ -z "$target_hosts" ] \
		&& target_hosts="$2" \
		    || target_hosts="$target_hosts $2"
	    shift 2
	    ;;

	-i|--install)
	    do_install=1
	    shift
	    ;;

	-S|--no-symlink)
	    no_symlink=1
	    shift
	    ;;
	
	-r|--relative)
	    relative_symlink=1
	    shift
	    ;;

	-V|--verbose)
	    verbose=1
	    shift
	    ;;

	-h|--help)
	    help
	    exit 0
	    ;;
	
	--)
	    shift
	    break
	    ;;
	
	*) ;;
    esac
done

# Find the location of the default macro set
spotfile_macros=$(find_macro_file "spotfile.m4")
[ $? -ne 0 ] && die "Could not find //spotfile.m4// in //\$SP_MACRO_PATH//"

[ -z "$do_install" -a "$no_symlink" = 1 ] &&
    log error "Warning: //--no-symlink// specified without //--install// (nonsensical)"

[ "$relative_symlink" = 1 -a "$no_symlink" = 1 ] &&
    log error "Warning: //--relative// and //--no-symlink// specified (nonsensical)"

if [ -z "$target_hosts" ]; then
    log verb "No hosts specified, using local hostname"
    target_hosts="$(hostname)"
fi

log verb "Using build host(s) //$target_hosts//"

if [ -z "$do_install" ]; then	# Build
    # The remaining unshifted arguments are package names
    for host in `echo $target_hosts | sed -e 's/:/\n/g'`; do
	for pkg in "$@"; do
	    log info "Working on package //$pkg// for //$host//"
	    
	    build_package "$pkg" "$host" "$spotfile_macros"
	done
    done
else				# Install configurations
    if [ "$(echo $target_hosts | awk '{print $1}')" != "$(echo $target_hosts)" ]; then
	die "More than one target host was specified\nOnly a single host should be specified, or none at all"
    fi

    # Strip whitespace
    host="$(echo $target_hosts)"

    log info "Using host //$host//"
    
    for pkg in "$@"; do
	log info "Installing package //$pkg//"
	install_package "$pkg" "$host" "$no_symlink" "$relative_symlink"
    done
fi
